/**
 * DataGenerator.java
 * 
 * Contains the thread that updates the charts for, Past Data, Neural Network Error and Actual Data.
 * Hourly consumption data is generated here.
 * 
 * TODO: Remove magic numbers
 * 
 */
package com.swin.energy;

import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import com.swin.energy.OptionsPanel.ACTUAL_DATA;
import com.swin.energy.OptionsPanel.ESTIMATE_FOR;
import com.swin.energy.OptionsPanel.PAST_DATA;

public class DataGenerator {

	private List<List<Double>> inputs;
	private List<Double> outputs;
	private List<String> area;
	private OptionsPanel optionsPanel;
	private ApplianceSetup setup;
	private NeuralNetwork nn;
	private List<Double> usageHistory;
	private int pastHours;
	private String filter;
	DecimalFormat twoDecimalPlaces = new DecimalFormat("#.##");
	
	public DataGenerator(OptionsPanel statsPanel) {
		this.setup = new ApplianceSetup();
		this.inputs = new ArrayList<List<Double>>(0);
		this.outputs = new ArrayList<Double>();
		this.optionsPanel = statsPanel;
		this.area = new ArrayList<String>();
		this.usageHistory = new ArrayList<Double>();
	}

	enum RUN_STATE {
		PAST, TRAIN, ACTUAL
	};

	enum SEASON {
		SUMMER, AUTUMN, WINTER, SPRING
	};

	synchronized private void updateFilter(String [] text, ESTIMATE_FOR estimation) {
		if (estimation == ESTIMATE_FOR.APPLIANCE){
			filter = text[0] + "," + text[1] + "," + text[2];
		} else if (estimation == ESTIMATE_FOR.HOUSE) {
			filter = text[0] + "," + text[1];
		} else if (estimation == ESTIMATE_FOR.REGION) {
			filter = text[0];
		}
		
	}
	
	// TODO: Refactor this method!!!
	synchronized void pastData(GraphPanel gPanel, int numberOfHours, boolean isDay, ESTIMATE_FOR estimation) {
		inputs.clear();
		double totalUsageForPastData1 = 0.0;
		SEASON season1 = null;
		int toPlot = 0;
		int xAxisValue = 0;
		double hourInFirstDay1 = -1;
		pastHours = numberOfHours;
		for (int i = 0; i < numberOfHours; i++) {
			season1 = (isDay) ? SEASON.WINTER : seasonFromHour(i);

			int hourlyUsage = 0;
			int hour = i % 24;

			if (i % 730 == 0) { // start of every month
				hourInFirstDay1 = 0;
			}
			if (hourInFirstDay1 >= 0) {
				if (hourInFirstDay1 > 24 * 2)
					hourInFirstDay1 = -1;
				else
					hourInFirstDay1++;
			}
			for (String a : area) {
				String [] st = a.split(",");
				String applianceName = st[2];
				
				if (i == 0) {
					updateFilter(st, estimation);
				} 
				
				if (estimation == ESTIMATE_FOR.ALL || a.contains(filter)) {
					boolean output = false;
					if (setup.isApplianceTurnedOn(applianceName,
							hour, season1)) {
						hourlyUsage += setup
								.getApplianceRate(applianceName);
						output = true;
					} else {
						output = false;
					}
	
					if (hourInFirstDay1 > 0 || isDay) {
						List<Double> temp = new ArrayList<Double>(2);
						temp.add((double) hour);
						temp.add((double) season1.ordinal());
						temp.add(setup
								.getApplianceNumber(applianceName));
						inputs.add(temp);
						if (output)
							outputs.add(1.0);
						else
							outputs.add(0.0);
					}
				}
			}
			if (isDay) {
				toPlot = hourlyUsage;
			} else {
				toPlot += hourlyUsage;
			}
			if (i % 730 == 0 || isDay) {
				gPanel.updateChart(xAxisValue, toPlot, null);
				usageHistory.add(new Double(toPlot));
				totalUsageForPastData1 += toPlot;
				toPlot = 0;
				xAxisValue++;
			}
		}
		optionsPanel.showUpdateButton();
		optionsPanel.setPastDataTotalValue(Double.toString(totalUsageForPastData1));
		
		optionsPanel.setMeanValue(PredictionUtilities.calcMean(usageHistory));
		optionsPanel.setMedianValue(PredictionUtilities.calcMedian(usageHistory));
	}
	
	void startUpdating(final GraphPanel gPanel, final int numberOfHours,
			final RUN_STATE state) {
		final PAST_DATA data = optionsPanel.getLengthOfWindow();
		final ESTIMATE_FOR est = optionsPanel.getEstimationFor();
		final ACTUAL_DATA act = optionsPanel.getActualWindowLength();
		new Thread(new Runnable() {

			@Override
			public void run() {
				gPanel.clearData();
				switch (state) {
				case PAST:
					switch (data) {
					case DAY:
						pastData(gPanel, 24, true, est);
						break;
					case YEAR:
						pastData(gPanel, 8760, false, est);
						break;
					default:
						break;
					}
					break;
				case TRAIN:
					switch (data) {
					case DAY:
						runTrainNN(gPanel, 500, 10);
						break;
					case YEAR:
						runTrainNN(gPanel, 500, 10);
						break;
					}
					break;
				case ACTUAL:
					runNNData(gPanel, act);
				default:
					break;
				}
			}

		}).start();
	}

	public double[][] getInputs() {
		double[][] temp = new double[this.inputs.size()][3];
		int i = 0;
		for (List<Double> d : this.inputs) {
			temp[i][0] = (double) d.get(0); // hour
			temp[i][1] = (double) d.get(1); // season
			temp[i][2] = (double) d.get(2); // appliance
			i++;

		}
		return temp;
	}

	// TODO: This might be too slow
	public double[][] getOutputs() {
		double[][] temp = new double[this.outputs.size()][1];
		int i = 0;
		for (Double d : this.outputs) {
			temp[i][0] = (double) d;
			i++;
		}
		return temp;
	}

	double booleanToDouble(boolean bool) {
		return bool ? 1.0 : 0.0;
	}

	boolean doubleToBoolean(double doub) {
		return doub > 0.5 ? true : false;
	}

	private SEASON seasonFromHour(int hourNumber) {
		if (hourNumber > 8760) {
			hourNumber %= 8760;
		}
		int hoursInSeason = 2190;
		SEASON season;
		if (hourNumber < hoursInSeason) {
			season = SEASON.SUMMER;
		} else if (hourNumber >= hoursInSeason
				&& hourNumber < hoursInSeason * 2) {
			season = SEASON.AUTUMN;
		} else if (hourNumber >= hoursInSeason * 2
				&& hourNumber < hoursInSeason * 3) {
			season = SEASON.WINTER;
		} else {
			season = SEASON.SPRING;
		}
		return season;
	}

	double[] calculateInputArray(double[] seasonAndHour) {
		// First 4 double values represent season
		double[] input = new double[33];

		double scaleFactor = 0.5;

		Arrays.fill(input, scaleFactor * -1);
		double hour = seasonAndHour[0];
		input[(int) hour] = scaleFactor;
		double season = seasonAndHour[1];
		input[(int) season + 24] = scaleFactor;
		input[(int) seasonAndHour[2] + 28] = scaleFactor;
		return input;
	}

	synchronized private void runTrainNN(GraphPanel gPanel, int nIterations,
			int epoc) {
		double[][] inputs = getInputs();
		double[][] outputs = getOutputs();
		int trial = 0;
		
		nn = new NeuralNetwork(33, 5, 1);
		nn.initialiseWeights();
		for (int it = 0; it < nIterations; it++) {
			double error = 0.0;
			for (int i = 0; i < inputs.length; i++) {
				double input[] = calculateInputArray(inputs[i]);
				double output[] = outputs[i];

				nn.forwardPropagation(input);
				error = +nn.backPropagation(output);
			}
			if (it % epoc == 0) {
				nn.updateWeights();
				gPanel.updateChart(trial, error, null);
				trial++;
			}
		}
	}

	// TODO: Simplify and possibly combine with pastData as the logic is very similar
	synchronized private void estimateAndActual(int numberOfHours, GraphPanel gPanel, boolean isMonthly) {
		inputs.clear();
		double monthlyUsage = 0.0;
		double monthlyEstimate = 0.0;
		int month = 0;
		double hourlyUsage = 0.0;
		double hourlyEstimate = 0.0;
		
		for (int i = 0; i < numberOfHours; i++) {
			hourlyUsage = 0.0;
			hourlyEstimate = 0.0;
			double[] estimate = null;
			int hour = i % 24;
			for (String a : area) {
				String applianceName = a.split(",")[2];
				SEASON hSeason = seasonFromHour(pastHours + i);
				
				if (setup.isApplianceTurnedOn(applianceName, hour, hSeason)) {
					hourlyUsage += setup.getApplianceRate(applianceName);
				}

				double[] input = { hour, (double) hSeason.ordinal(),setup.getApplianceNumber(applianceName) };
				estimate = nn.forwardPropagation(calculateInputArray(input));
				System.out.println(estimate[0]);
				if (estimate[0] >= 0.5) {
					hourlyEstimate += setup.getApplianceRate(applianceName);
				}

			}
			
			if(isMonthly) {
				
				monthlyUsage += hourlyUsage;
				monthlyEstimate += hourlyEstimate;
				
				if(i % 730 == 0 && i > 0) { // new month
					month++;
					gPanel.updateChart(month, monthlyUsage, monthlyEstimate);
					monthlyUsage = 0.0;
					monthlyEstimate = 0.0;
				}
			} else {
				gPanel.updateChart(i, hourlyUsage, hourlyEstimate);
			}			
		}	
		if(isMonthly)
		{
			
			optionsPanel.setActualValueText(twoDecimalPlaces.format(monthlyUsage));
			optionsPanel.setEstimateValueText(twoDecimalPlaces.format(monthlyEstimate));
					
			
		}
		else
		{
			
			optionsPanel.setActualValueText(twoDecimalPlaces.format(hourlyUsage));
			optionsPanel.setEstimateValueText(twoDecimalPlaces.format(hourlyEstimate));
		}
		
	}
	
	synchronized private void runNNData(GraphPanel gPanel, ACTUAL_DATA actData) {
		switch (actData) {
		case HOUR:
			estimateAndActual(1, gPanel, false);
			break;
		case DAY:
			estimateAndActual(24, gPanel, false);
			break;
		case WEEK:
			estimateAndActual(168, gPanel, false);
			break;
		case SIX_MONTHS:
			estimateAndActual(4380, gPanel, true);
			break;
		case YEAR:
			estimateAndActual(8760, gPanel, true);
		default:
			break;
		}
	}

	public void setArea(List<String> area) {
		this.area = area;
	}

}
